package game;

import java.awt.AlphaComposite;
import java.awt.Canvas;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.MemoryImageSource;

import javax.swing.JFrame;
import javax.swing.JPanel;

import raytracer.Raytracer;
import raytracer.Vector3f;
import scene.Color;
import scene.Material;
import scene.Scene;
import scene.SceneLoader;
import scene.Shape;
import scene.Sphere;

public class Game extends Canvas implements MouseMotionListener, MouseListener {
	public enum GameState {AIM, SIM, GAMEOVER, COMPLETE, LEVEL_COMPLETE};
	
	private Scene scene;
	private Raytracer[] raytracer;
	private Thread[] raythreads;
	private float fps;
	private Sphere catcher;
	private Sphere doomBall;
	private Vector3f lastDoomBallPos = new Vector3f();
	private GameState state = GameState.AIM;
	private int mx, my;
	private GradientPaint AimerGradient = new GradientPaint(0, 0, java.awt.Color.BLUE, 200, 200, java.awt.Color.WHITE);
	private BufferStrategy strategy;
	private TimedMessage timedMsg;
	private int curLevel = 0;
	private int score = 0;
	private Vector3f camPosTarget;
	private float camHATarget;
	private float speedMod = 1f;
	private boolean lastBall;
	private boolean fireDoomBall = false;

	public Game() {
		JFrame container = new JFrame("Raytracer Peggle");

		JPanel panel = (JPanel) container.getContentPane();
		panel.setPreferredSize(new Dimension(Consts.SCR_W, Consts.SCR_H));
		panel.setLayout(null);

		setBounds(0, 0, Consts.SCR_W, Consts.SCR_H);
		panel.add(this);

		setIgnoreRepaint(true);

		container.pack();
		container.setResizable(false);
		container.setVisible(true);

		container.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				System.exit(0);
			}
		});

		// keys = new Keys();
		// addKeyListener(new KeyInputHandler());
		addMouseListener(this);
		addMouseMotionListener(this);
		//
		// this.mouseEventStack = new LinkedList<MouseEvent>();
		// this.mouseEventTypeStack = new LinkedList<Integer>();
		//
		int[] pixels = new int[16 * 16];
		Image image = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(16, 16, pixels, 0, 16));
		Cursor transparentCursor = Toolkit.getDefaultToolkit().createCustomCursor(image, new java.awt.Point(0, 0),
				"invisibleCursor");
		// this.setCursor(transparentCursor);

		requestFocus();

		 createBufferStrategy(2);
		 strategy = getBufferStrategy();

		// scene = new Scene();
		loadLevel();
		
		timedMsg = new TimedMessage("GO!", 3);
	}

	private void loadLevel() {
		scene = SceneLoader.loadScene("level" + curLevel + ".scn");
		System.out.println(scene);
		
		System.out.println("Loaded level " + curLevel);
		
		if(scene == null) {
			scene = new Scene();
			state = GameState.COMPLETE;
			timedMsg = new TimedMessage("Game complete!", 10000);
			System.out.println("Did not exists. No more levels?");
		}
		
		scene.renderInfo.width = Consts.REN_W;
		scene.renderInfo.height = Consts.REN_H;
		
		setUpCatcher();
		setUpDoomBall();
		
		setUpRaytracers();
	}

	private void setUpDoomBall() {
		Material mat = new Material(new Color(0,0,0), new Color(1f, 1f, 0f), new Color(.4f, .4f, .2f), 100f, new Color(0,0,0), 1f);
		doomBall = new Sphere(1.2f, 0, 17, 0, mat);
		scene.shapes.add(doomBall);
		lastDoomBallPos.copyFrom(doomBall.pos);
		speedMod = 1f;
		camHATarget = 30;
		scene.camera.dir.copyFrom(scene.camera.oriDir);
		lastBall = false;
	}

	private void setUpCatcher() {
		Material mat = new Material(new Color(0,0,0), new Color(1f, 0, 1f), new Color(.2f, .2f, .2f), 100f, new Color(0,0,0), 1f);
		catcher = new Sphere(5, 0, -19, 0, mat);
		catcher.v = new Vector3f(1,0,0);
		scene.shapes.add(catcher);
	}

	private void setUpRaytracers() {
		raythreads = new Thread[Consts.NUM_THREADS];
		raytracer = new Raytracer[Consts.NUM_THREADS];

		for (int t = 0; t < Consts.NUM_THREADS; t++) {
			raytracer[t] = new Raytracer(scene, Consts.T_SLICE * t, Consts.T_SLICE * (t + 1) - 1, false, 2);

			raythreads[t] = new Thread(raytracer[t]);
			raythreads[t].start();
		}
	}

	public void gameLoop() {
		float fpsc = 0;
		long fpst = System.currentTimeMillis();
		long loopt = System.nanoTime();

		float c = 0;
		camPosTarget = scene.camera.pos;
		camHATarget = scene.camera.halfangle;
		
		while (true) {
			if(fireDoomBall && state == GameState.AIM) {
				fireDoomBall = false;
				fireDoomBall();
				state = GameState.SIM;
			}
			
			float t = (System.nanoTime() - loopt) / 1e8f * speedMod;
			loopt = System.nanoTime();
			
			updateSpheres(t);
			updateCatcher(t);
			
			switch(state) {
			case SIM:
			case LEVEL_COMPLETE:
				updateDoomBall(t);
				break;
			}
			
			if(camPosTarget.distance(scene.camera.pos) > 0.3f) {
				Vector3f towards = camPosTarget.subtract(scene.camera.pos);
				scene.camera.pos.addLocal(towards.scale(0.01f));
			}
			if(Math.abs(camHATarget - scene.camera.halfangle) > 1) {
				scene.camera.halfangle += (camHATarget - scene.camera.halfangle) * 0.01f;
			} else {
				camHATarget = 30;
			}

			Graphics2D g = (Graphics2D) strategy.getDrawGraphics();

			g.drawImage(renderScene(), 0, 0, Consts.SCR_W - 1, Consts.SCR_H - 1, 0, 0, Consts.REN_W - 1, Consts.REN_H - 1,
					null);
			renderTimedMessage(g);
			renderHUD(g);
			
			switch(state) {
			case AIM:
				renderAim(g);
				break;
			}
			
			strategy.show();
			
			if(scene.lives < 0) {
				timedMsg = new TimedMessage("GAME OVER", 10);
				state = GameState.GAMEOVER;
			}
			if(scene.targets <= 0) {
				timedMsg = new TimedMessage("Good job!", 5);
				catcher.collidable = false;
				catcher.a.y = Consts.GRAVITY;
				state = GameState.LEVEL_COMPLETE;
			}

			fpsc++;
			if (fpst + 3000 < System.currentTimeMillis()) {
				fps = fpsc / 3f;
				fpsc = 0;
				fpst = System.currentTimeMillis();
			}
		}
	}

	private void renderTimedMessage(Graphics2D g) {
		if(timedMsg == null) return;
		
		Font font = new Font("Tahoma", Font.BOLD, 80);
		Font oldFont = g.getFont();
		g.setFont(font);
		g.setColor(java.awt.Color.white);
		
		g.drawString(timedMsg.text, (int) (Consts.SCR_W / 2 - g.getFontMetrics().getStringBounds(timedMsg.text, g).getWidth() / 2), Consts.SCR_H / 2);		
		
		g.setFont(oldFont);
		
		if(timedMsg.endTime < System.currentTimeMillis()) {
			timedMsg = null;
		}
	}

	private void renderHUD(Graphics2D g) {
		Font font = new Font("Tahoma", Font.PLAIN, 30);
		Font oldFont = g.getFont();
		g.setFont(font);
		g.setColor(java.awt.Color.WHITE);

		g.drawString("Balls: " + scene.lives, (int) 10, 30 );
		g.drawString("Score: " + score, (int) 10, 60 );	
		g.drawString("fps: " + fps, (int) 10, 90 );		
		
	}

	private void updateDoomBall(float t) {
		Vector3f step = doomBall.pos.subtract(lastDoomBallPos).normalize().scale(Consts.TIMESTEP);
		lastDoomBallPos.copyFrom(doomBall.pos);
		
		Vector3f testPos = doomBall.pos; 
		int steps = 0, numSteps = (int) (doomBall.pos.subtract(lastDoomBallPos).length() / step.length() /*+ 1f*/);
		
		do {
			for(Shape sh : scene.shapes) {
				if(sh.equals(doomBall) || ! sh.collidable) continue;
				
				if(testPos.distance(sh.pos) < doomBall.collisionRadius + sh.collisionRadius) {				
					if(sh.equals(catcher) ) {
						setUpDoomBall();
						state = GameState.AIM;
						timedMsg = new TimedMessage("Extra ball!", 3);
						score += 100;
						break;
					} else {
						doomBall.pos.copyFrom(testPos);
						Vector3f b = doomBall.pos.subtract(sh.pos).normalize();
						doomBall.v = b.scale(doomBall.v.length() * Consts.DAMPENING);
						sh.collidable = false;
						scene.targets --;
						score += sh.score;
						if(sh.givelife) {
							scene.lives ++;
						}
						startBallDeath(sh);
					}
				}
			}
		
			testPos.addLocal(step);
		} while(steps++ < numSteps);
		
		if(doomBall.pos.y < - 20) {
			if(state == GameState.LEVEL_COMPLETE) {
				curLevel ++;
				loadLevel();
				setUpDoomBall();
				state = GameState.AIM;
			} else {
				setUpDoomBall();
				scene.lives --;
				state = GameState.AIM;
			}
		}
		
		if(doomBall.pos.x > 20) {
			doomBall.v.x = - Math.abs(doomBall.v.x);
			doomBall.v.y *= -.5f;
		} else if(doomBall.pos.x < -20) {
			doomBall.v.x = Math.abs(doomBall.v.x);
			doomBall.v.y *= -.5f;
		}
		
		if(lastBall) {
			scene.camera.dir.copyFrom(doomBall.pos);
			//Ray.prepare(scene);
		}
	}

	private void startBallDeath(Shape sh) {
		sh.a.y = Consts.GRAVITY;
		sh.v.z = (float) (Math.random() * 4f - 2f);
		sh.v.y = 3f;
	}

	private void renderAim(Graphics2D g) {
		int cx = Consts.SCR_W / 2;
		int[] xPoints = {cx - 20, cx + 20, mx};
		int[] yPoints = {0, 0, my};
		
		Composite origComp = g.getComposite();
		
		g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
		g.setPaint(AimerGradient );
		g.fillPolygon(xPoints, yPoints, 3);
		
		g.setComposite(origComp);
	}

	private void updateCatcher(float t) {
		if(catcher.pos.x > 20) {
			catcher.v.x = -1;
		} else if(catcher.pos.x < -20) {
			catcher.v.x = 1;
		}
	}

	private void updateSpheres(float t) {
		for (Shape sh : scene.shapes) {
			sh.v.addLocal(sh.a.scale(t));
			sh.pos.addLocal(sh.v.scale(t));
		}
	}

	private Image renderScene() {
		for (int i = 0; i < Consts.NUM_THREADS; i++) {
			raytracer[i].startRender();
		}

		for (int i = 0; i < Consts.NUM_THREADS; i++) {
			raytracer[i].waitRender();
		}

		BufferedImage img = new BufferedImage(scene.renderInfo.width, scene.renderInfo.height,
				BufferedImage.TYPE_INT_RGB);
		Graphics2D g = (Graphics2D) img.getGraphics();

		// Put image together
		for (int t = 0; t < Consts.NUM_THREADS; t++) {
			int dx1 = Consts.T_SLICE * t - 1;
			int dy1 = 0;
			int dx2 = Consts.T_SLICE * (t + 1) - 1;
			int dy2 = scene.renderInfo.height;
			int sx1 = 0;
			int sy1 = 0;
			int sx2 = scene.renderInfo.width / Consts.NUM_THREADS;
			int sy2 = scene.renderInfo.height;
			
			g.drawImage(raytracer[t].getImg(), dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
		}
		
		return img;
	}
	
	private void fireDoomBall() {
		if(state != GameState.AIM) return;
		
		doomBall.a.y = Consts.GRAVITY;
		
		float percH = 1f - ((float) my) / ((float) Consts.SCR_H);
		
		float halfW = Consts.SCR_W / 2f;
		doomBall.v.x = (((float)mx - halfW) / halfW * 3f) * (percH + 0.3f);
		
		if(scene.lives == 0) {
			lastBall = true;
			camHATarget = 10f;
			speedMod = 0.2f;
//			doomBall.v.x /= speedMod;
		}
	}

	@Override
	public void mouseDragged(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseMoved(MouseEvent me) {
		this.mx = me.getX();
		this.my = me.getY();
	}

	@Override
	public void mouseClicked(MouseEvent arg0) {
	}

	@Override
	public void mouseEntered(MouseEvent arg0) {
	}

	@Override
	public void mouseExited(MouseEvent arg0) {
	}

	@Override
	public void mousePressed(MouseEvent me) {
		if(state == GameState.AIM && me.getButton() == MouseEvent.BUTTON1) {			
			fireDoomBall  = true;
		}
	}

	@Override
	public void mouseReleased(MouseEvent arg0) {
	}


}
